/*******************************************************************************
 * Copyright (c) 2012 Alexandr Tsvetkov.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the GNU Public License v2.0
 * which accompanies this distribution, and is available at
 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
 * 
 * Contributors:
 *     Alexandr Tsvetkov - initial API and implementation
 * 
 * Project:
 *     TAO Library
 * 
 * File name:
 *     HtmlParser.java
 *     
 * License agreement:
 *
 * 1. This code is published AS IS. Author is not responsible for any damage that can be
 *    caused by any application that uses this code.
 * 2. Author does not give a garantee, that this code is error free.
 * 3. This code can be used in NON-COMMERCIAL applications AS IS without any special
 *    permission from author.
 * 4. This code can be modified without any special permission from author IF AND ONLY IF
 *    this license agreement will remain unchanged.
 * 5. SPECIAL PERMISSION for this code usage in COMMERCIAL application SHOULD be obtained
 *    from author.
 ******************************************************************************/
package ua.at.tsvetkov.parsers;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.URLConnection;

import ua.at.tsvetkov.util.Log;


/**
 * Парсер для HTML и XML содержимого веб страницы. Проверяет все данные и в случае ошибки выдает
 * полную информацию по ее местоположению в коде и обрабатываемом материале. Приблизительно на треть
 * медленнее чем <b>HtmlParserQuick</b>. Рекомендуется использовать для отладки кода.
 * 
 * @author A.Tsvetkov 2010 http://tsvetkov.at.ua mailto:al@ukr.net
 */

public class HtmlParser {

	public static final String		WINDOWS_1251								= "windows-1251";
	public static final String		UTF_8											= "utf-8";

	private static final String	STRING_IS_EMPTY_OR_NULL					= "string is empty or NULL.";
	private static final String	FINAL_STRING_IS_EMPTY_OR_NULL			= "Final " + STRING_IS_EMPTY_OR_NULL;
	private static final String	STARTING_STRING_IS_EMPTY_OR_NULL		= "Starting " + STRING_IS_EMPTY_OR_NULL;
	private static final String	SEARCH_STRING_IS_EMPTY_OR_NULL		= "Search " + STRING_IS_EMPTY_OR_NULL;
	private static final String	DOES_NOT_CONTAIN_THE_SEARCH_STRING	= "Does not contain the search string.";
	private static final String	ERROR_READ_URL								= "Error read url.";
	private static final String	END_OF_STREAM								= "End of stream.";

	private String						charset										= UTF_8;
	private InputStreamReader		br												= null;
	private URL							url											= null;
	private BufferedReader			in												= null;
	private URLConnection			connection									= null;
	private InputStream				is												= null;
	private String						urlStr										= "";
	private String						lastLine;
	private int							stringNumber;
	private long						time;

	/**
	 * Создает парсер для обработки материала выдаваемого по ссылке в определенной кодировке
	 * 
	 * @param urlStr ссылка на материал
	 * @param charset кодировка (WINDOWS_1251, UTF_8)
	 */
	public HtmlParser(String urlStr, String charset) {
		this.urlStr = urlStr;
		this.charset = charset;
		stringNumber = 0;
	}

	/**
	 * Создает парсер для обработки материала выдаваемого по ссылке в кодировке UTF-8
	 * 
	 * @param urlStr ссылка на материал
	 */
	public HtmlParser(String urlStr) {
		this.urlStr = urlStr;
	}

	/**
	 * Создает соединение с выходом по таймауту
	 * 
	 * @param timeout время таймаута в миллисекундах
	 * @return true если соединение успешно
	 * @throws HtmlParserException
	 */
	public void connect(int timeout) throws HtmlParserException {
		time = System.currentTimeMillis();
		try {
			url = new URL(urlStr);
			connection = (URLConnection) url.openConnection();
			connection.setConnectTimeout(timeout);
			is = connection.getInputStream();
			br = new InputStreamReader(is, charset);
			in = new BufferedReader(br);
		} catch (SocketTimeoutException e) {
			throw new HtmlParserException(stringNumber, "Timeout. Connection error.", url);
		} catch (IOException e) {
			throw new HtmlParserException(stringNumber, "Connection error.", url);
		}
	}

	/**
	 * Закрывает соединение парсера
	 * 
	 * @return true если закрытие успешно
	 * @throws HtmlParserException
	 */
	public void close() throws HtmlParserException {
		try {
			in.close();
			br.close();
			is.close();
			Log.d("Parsing time " + (System.currentTimeMillis() - time));
		} catch (IOException e) {
			throw new HtmlParserException(stringNumber, "Close connection error.", url);
		}
	}

	/**
	 * Читает содержимое до тех пор, пока в содержимом не обнаружится искомая строка.
	 * 
	 * @param stringForSearch строка для поиска
	 * @return строку в которой есть искомая строка
	 * @throws HtmlParserException если искомая строка не найдена и достигнут конец потока.
	 */
	public String search(String stringForSearch) throws HtmlParserException {
		if (stringForSearch == null || stringForSearch.length() == 0) {
			throw new HtmlParserException(stringNumber, SEARCH_STRING_IS_EMPTY_OR_NULL);
		}
		String st = "";
		try {
			while (!st.contains(stringForSearch)) {
				stringNumber++;
				st = in.readLine();
				if (st == null) {
					throw new HtmlParserException(stringNumber, END_OF_STREAM, stringForSearch);
				}
				lastLine = st;
			}
			return st;
		} catch (IOException e) {
			throw new HtmlParserException(stringNumber, ERROR_READ_URL, url);
		}
	}

	/**
	 * Читает содержимое до тех пор, пока в содержимом не обнаружатся начальная и конечная подстроки.
	 * Возвращает строку лежащую между ними.
	 * 
	 * @param stStart начальная подстрока
	 * @param stEnd конечная подстрока
	 * @return подстрока заключенная между начальной и конечной подстроками
	 * @throws HtmlParserException если искомая строка не найдена и достигнут конец потока.
	 */
	public String searchSubstring(String stStart, String stEnd) throws HtmlParserException {
		String st = "";
		try {
			if (stStart == null || stStart.equals("")) {
				throw new HtmlParserException(stringNumber, STARTING_STRING_IS_EMPTY_OR_NULL);
			}
			if (stEnd == null || stEnd.equals("")) {
				throw new HtmlParserException(stringNumber, FINAL_STRING_IS_EMPTY_OR_NULL);
			}
			while (!st.contains(stStart) && !st.contains(stEnd)) {
				stringNumber++;
				st = in.readLine();
				if (st == null) {
					throw new HtmlParserException(stringNumber, END_OF_STREAM);
				}
				lastLine = st;
			}
			return getSubstring(st, stStart, stEnd);
		} catch (IOException e) {
			throw new HtmlParserException(stringNumber, ERROR_READ_URL, url);
		}
	}

	/**
	 * Читает содержимое до тех пор, пока в содержимом не обнаружатся искомая подстрока - маркер.
	 * Возвращает строку лежащую между начальной и конечной подстроками.
	 * 
	 * @param marker маркер - подстрока для поиска
	 * @param stStart начальная подстрока
	 * @param stEnd конечная подстрока
	 * @return подстрока заключенная между начальной и конечной подстроками
	 * @throws HtmlParserException если искомая строка не найдена и достигнут конец потока.
	 */
	public String searchMarkerAndGetSubstring(String marker, String stStart, String stEnd) throws HtmlParserException {
		String st = search(marker);
		return getSubstring(st, stStart, stEnd);
	}

	/**
	 * Читает следующую строку
	 * 
	 * @return прочитанная строка
	 * @throws HtmlParserException если искомая строка не найдена и достигнут конец потока.
	 */
	public String readLine() throws HtmlParserException {
		String st = "";
		try {
			stringNumber++;
			st = in.readLine();
			if (st == null) {
				throw new HtmlParserException(stringNumber, END_OF_STREAM);
			}
			lastLine = st;
			return st;
		} catch (IOException e) {
			throw new HtmlParserException(stringNumber, ERROR_READ_URL, url);
		}
	}

	/**
	 * Возвращает последнюю прочтенную строку
	 * 
	 * @return последняя прочитанная строка
	 */
	public String getLastLine() {
		return lastLine;
	}

	/**
	 * Анализирует переданную строку st и возвращает подстроку, заключенную между строками stStart и
	 * stEnd
	 * 
	 * @param st строка для анализа
	 * @param stStart начальный текст ограничивающий нужную подстроку слева
	 * @param stEnd конечный текст ограничивающий нужную подстроку справа
	 * @return искомая подстрока
	 * @throws HtmlParserException если строка не надена
	 */
	public String getSubstring(String st, String stStart, String stEnd) throws HtmlParserException {
		int beginInd = 0;
		int endInd = 0;
		if (st == null || st.equals("")) {
			throw new HtmlParserException(stringNumber, STRING_IS_EMPTY_OR_NULL, st);
		}
		if (stStart == null || stStart.equals("")) {
			throw new HtmlParserException(stringNumber, STARTING_STRING_IS_EMPTY_OR_NULL, st);
		}
		if (stEnd == null || stEnd.equals("")) {
			throw new HtmlParserException(stringNumber, FINAL_STRING_IS_EMPTY_OR_NULL, st);
		}
		beginInd = st.indexOf(stStart);
		if (beginInd < 1) {
			throw new HtmlParserException(stringNumber, DOES_NOT_CONTAIN_THE_SEARCH_STRING, st);
		}
		beginInd += stStart.length();
		endInd = st.indexOf(stEnd);
		if (endInd < 1) {
			throw new HtmlParserException(stringNumber, DOES_NOT_CONTAIN_THE_SEARCH_STRING, st);
		}
		if (endInd < beginInd) {
			throw new HtmlParserException(stringNumber, "String \"" + stStart + "\" located after string \"" + stEnd + "\"", st);
		}
		return st.substring(beginInd, endInd);
	}

}
